/* ----------------------------------------------------------------------------
 *         ATMEL Microcontroller Software Support
 * ----------------------------------------------------------------------------
 * Copyright (c) 2008, Atmel Corporation
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the disclaimer below.
 *
 * Atmel's name may not be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * DISCLAIMER: THIS SOFTWARE IS PROVIDED BY ATMEL "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE
 * DISCLAIMED. IN NO EVENT SHALL ATMEL BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ----------------------------------------------------------------------------
 */

/** \file
 * \addtogroup usbd_msd
 *@{
 */

/*-----------------------------------------------------------------------------
 *      Includes
 *-----------------------------------------------------------------------------*/

#include "SBCMethods.h"
#include "MSDDStateMachine.h"

/*-----------------------------------------------------------------------------
 *      Internal functions
 *-----------------------------------------------------------------------------*/
/**
 * Returns the expected transfer length and direction (IN, OUT or don't care)
 * from the host point-of-view.
 * \param  cbw     Pointer to the CBW to examinate
 * \param  pLength Expected length of command
 * \param  pType   Expected direction of command
 */
static void MSDD_GetCommandInformation(MSCbw *cbw,
                                       unsigned int  *length,
                                       unsigned char *type)
{
    /* Expected host transfer direction and length */
    (*length) = cbw->dCBWDataTransferLength;

    if (*length == 0) {

        (*type) = MSDD_NO_TRANSFER;
    }
    else if ((cbw->bmCBWFlags & MSD_CBW_DEVICE_TO_HOST) != 0) {

        (*type) = MSDD_DEVICE_TO_HOST;
    }
    else {

        (*type) = MSDD_HOST_TO_DEVICE;
    }
}

/**
 * Pre-processes a command by checking the differences between the host and
 * device expectations in term of transfer type and length.
 * Once one of the thirteen cases is identified, the actions to do during the
 * post-processing phase are stored in the dCase variable of the command
 * state.
 * \param  pMsdDriver Pointer to a MSDDriver instance
 * \return 1 if the command is supported, false otherwise
 */
static unsigned char MSDD_PreProcessCommand(MSDDriver *pMsdDriver)
{
    unsigned int        hostLength = 0;
    unsigned int        deviceLength = 0;
    unsigned char       hostType;
    unsigned char       deviceType;
    unsigned char       isCommandSupported;
    MSDCommandState *commandState = &(pMsdDriver->commandState);
    MSCsw           *csw = &(commandState->csw);
    MSCbw           *cbw = &(commandState->cbw);
    MSDLun          *lun = &(pMsdDriver->luns[(unsigned char) cbw->bCBWLUN]);

    /* Get information about the command */
    /* Host-side */
    MSDD_GetCommandInformation(cbw, &hostLength, &hostType);

    /* Device-side */
    isCommandSupported = SBC_GetCommandInformation(cbw->pCommand,
                                                   &deviceLength,
                                                   &deviceType,
                                                   lun);

    /* Initialize data residue and result status */
    csw->dCSWDataResidue = 0;
    csw->bCSWStatus = MSD_CSW_COMMAND_PASSED;

    /* Check if the command is supported */
    if (isCommandSupported) {

        /* Identify the command case */
        if(hostType == MSDD_NO_TRANSFER) {

            /* Case 1  (Hn = Dn) */
            if(deviceType == MSDD_NO_TRANSFER) {

                /*TRACE_WARNING("Case 1\n\r"); */
                commandState->postprocess = 0;
                commandState->length = 0;
            }
            else if(deviceType == MSDD_DEVICE_TO_HOST) {

                /* Case 2  (Hn < Di) */
                TRACE_WARNING(
                    "MSDD_PreProcessCommand: Case 2\n\r");
                commandState->postprocess = MSDD_CASE_PHASE_ERROR;
                commandState->length = 0;
            }
            else { /*if(deviceType == MSDD_HOST_TO_DEVICE) { */

                /* Case 3  (Hn < Do) */
                TRACE_WARNING(
                    "MSDD_PreProcessCommand: Case 3\n\r");
                commandState->postprocess = MSDD_CASE_PHASE_ERROR;
                commandState->length = 0;
            }
        }

        /* Case 4  (Hi > Dn) */
        else if(hostType == MSDD_DEVICE_TO_HOST) {

            if(deviceType == MSDD_NO_TRANSFER) {

                TRACE_WARNING(
                    "MSDD_PreProcessCommand: Case 4\n\r");
                commandState->postprocess = MSDD_CASE_STALL_IN;
                commandState->length = 0;
                csw->dCSWDataResidue = hostLength;
            }
            else if(deviceType == MSDD_DEVICE_TO_HOST) {

                if(hostLength > deviceLength) {

                    /* Case 5  (Hi > Di) */
                    TRACE_WARNING(
                        "MSDD_PreProcessCommand: Case 5\n\r");
                    commandState->postprocess = MSDD_CASE_STALL_IN;
                    commandState->length = deviceLength;
                    csw->dCSWDataResidue = hostLength - deviceLength;
                }
                else if(hostLength == deviceLength) {

                    /* Case 6  (Hi = Di) */
                    commandState->postprocess = 0;
                    commandState->length = deviceLength;
                }
                else { /*if(hostLength < deviceLength) { */

                    /* Case 7  (Hi < Di) */
                    TRACE_WARNING(
                        "MSDD_PreProcessCommand: Case 7\n\r");
                    commandState->postprocess = MSDD_CASE_PHASE_ERROR;
                    commandState->length = hostLength;
                }
            }
            else { /*if(deviceType == MSDD_HOST_TO_DEVICE) { */

                /* Case 8  (Hi <> Do) */
                TRACE_WARNING(
                    "MSDD_PreProcessCommand: Case 8\n\r");
                commandState->postprocess =
                    MSDD_CASE_STALL_IN | MSDD_CASE_PHASE_ERROR;
                commandState->length = 0;
            }
        }
        else if(hostType == MSDD_HOST_TO_DEVICE) {

            if(deviceType == MSDD_NO_TRANSFER) {

                /* Case 9  (Ho > Dn) */
                TRACE_WARNING(
                    "MSDD_PreProcessCommand: Case 9\n\r");
                commandState->postprocess = MSDD_CASE_STALL_OUT;
                commandState->length = 0;
                csw->dCSWDataResidue = hostLength;
            }
            else if(deviceType == MSDD_DEVICE_TO_HOST) {

                /* Case 10 (Ho <> Di) */
                TRACE_WARNING(
                    "MSDD_PreProcessCommand: Case 10\n\r");
                commandState->postprocess =
                    MSDD_CASE_STALL_OUT | MSDD_CASE_PHASE_ERROR;
                commandState->length = 0;
            }
            else { /*if(deviceType == MSDD_HOST_TO_DEVICE) { */

                if(hostLength > deviceLength) {

                    /* Case 11 (Ho > Do) */
                    TRACE_WARNING(
                        "MSDD_PreProcessCommand: Case 11\n\r");
                    commandState->postprocess = MSDD_CASE_STALL_OUT;
/*                    commandState->length = deviceLength; */
/*                    csw->dCSWDataResidue = hostLength - deviceLength; */
                    commandState->length = 0;
                    csw->dCSWDataResidue = deviceLength;
                }
                else if(hostLength == deviceLength) {

                    /* Case 12 (Ho = Do) */
                    /*TRACE_WARNING( */
                    /*    "MSDD_PreProcessCommand: Case 12\n\r"); */
                    commandState->postprocess = 0;
                    commandState->length = deviceLength;
                }
                else { /*if(hostLength < deviceLength) { */

                    /* Case 13 (Ho < Do) */
                    TRACE_WARNING(
                        "MSDD_PreProcessCommand: Case 13\n\r");
                    commandState->postprocess = MSDD_CASE_PHASE_ERROR;
                    commandState->length = hostLength;
                }
            }
        }
    }

    return isCommandSupported;
}

/**
 * Post-processes a command given the case identified during the
 * pre-processing step.
 * Depending on the case, one of the following actions can be done:
 *           - Bulk IN endpoint is stalled
 *           - Bulk OUT endpoint is stalled
 *           - CSW status set to phase error
 * \param  pMsdDriver Pointer to a MSDDriver instance
 * \return If the device is halted
 */
static unsigned char MSDD_PostProcessCommand(MSDDriver *pMsdDriver)
{
    MSDCommandState *commandState = &(pMsdDriver->commandState);
    MSCsw           *csw = &(commandState->csw);
    unsigned char haltStatus = 0;

    /* STALL Bulk IN endpoint ? */
    if ((commandState->postprocess & MSDD_CASE_STALL_IN) != 0) {

        TRACE_INFO_WP("StallIn ");
        //MSDD_Halt(MSDD_CASE_STALL_IN);
        USBD_Halt(commandState->pipeIN);
        haltStatus = 1;
    }

    /* STALL Bulk OUT endpoint ? */
    if ((commandState->postprocess & MSDD_CASE_STALL_OUT) != 0) {

        TRACE_INFO_WP("StallOut ");
        //MSDD_Halt(MSDD_CASE_STALL_OUT);
        USBD_Halt(commandState->pipeOUT);
        haltStatus = 1;
    }

    /* Set CSW status code to phase error ? */
    if ((commandState->postprocess & MSDD_CASE_PHASE_ERROR) != 0) {

        TRACE_INFO_WP("PhaseErr ");
        csw->bCSWStatus = MSD_CSW_PHASE_ERROR;
    }

    return haltStatus;
}

/**
 * Processes the latest command received by the %device.
 * \param  pMsdDriver Pointer to a MSDDriver instance
 * \return 1 if the command has been completed, false otherwise.
 */
static unsigned char MSDD_ProcessCommand(MSDDriver * pMsdDriver)
{
    unsigned char   status;
    MSDCommandState *commandState = &(pMsdDriver->commandState);
    MSCbw           *cbw = &(commandState->cbw);
    MSCsw           *csw = &(commandState->csw);
    MSDLun          *lun = &(pMsdDriver->luns[(unsigned char) cbw->bCBWLUN]);
    unsigned char   isCommandComplete = 0;

    /* Check if LUN is valid */
    if (cbw->bCBWLUN > pMsdDriver->maxLun) {

        TRACE_WARNING(
            "MSDD_ProcessCommand: LUN %d not exist\n\r", cbw->bCBWLUN);
        status = MSDD_STATUS_ERROR;
    }
    else {

        /* Process command */
        if (pMsdDriver->maxLun > 0) {

            TRACE_INFO_WP("LUN%d ", cbw->bCBWLUN);
        }

        status = SBC_ProcessCommand(lun, commandState);
    }

    /* Check command result code */
    if (status == MSDD_STATUS_PARAMETER) {

        TRACE_WARNING(
            "MSDD_ProcessCommand: Unknown cmd 0x%02X\n\r",
            cbw->pCommand[0]);

        /* Update sense data */
        SBC_UpdateSenseData(&(lun->requestSenseData),
                            SBC_SENSE_KEY_ILLEGAL_REQUEST,
                            SBC_ASC_INVALID_FIELD_IN_CDB,
                            0);

        /* Result codes */
        csw->bCSWStatus = MSD_CSW_COMMAND_FAILED;
        isCommandComplete = 1;

        /* stall the request, IN or OUT */
        if (((cbw->bmCBWFlags & MSD_CBW_DEVICE_TO_HOST) == 0)
            && (cbw->dCBWDataTransferLength > 0)) {

            /* Stall the OUT endpoint : host to device */
            /* MSDD_Halt(MSDD_CASE_STALL_OUT); */
            commandState->postprocess = MSDD_CASE_STALL_OUT;
            TRACE_INFO_WP("StaOUT ");
        }
        else {

            /* Stall the IN endpoint : device to host */
            /* MSDD_Halt(MSDD_CASE_STALL_IN); */
            commandState->postprocess = MSDD_CASE_STALL_IN;
            TRACE_INFO_WP("StaIN ");
        }
    }
    else if (status == MSDD_STATUS_ERROR) {

        TRACE_WARNING("MSD_ProcessCommand: Cmd %x fail\n\r",
                   ((SBCCommand*)commandState->cbw.pCommand)->bOperationCode);

        /* Update sense data */
        SBC_UpdateSenseData(&(lun->requestSenseData),
                            SBC_SENSE_KEY_MEDIUM_ERROR,
                            SBC_ASC_INVALID_FIELD_IN_CDB,
                            0);

        /* Result codes */
        csw->bCSWStatus = MSD_CSW_COMMAND_FAILED;
        isCommandComplete = 1;
    }
    else if (status == MSDD_STATUS_RW) {

        csw->bCSWStatus = MSD_CSW_COMMAND_FAILED;
        isCommandComplete = 1;
    }
    else {

        /* Update sense data */
        SBC_UpdateSenseData(&(lun->requestSenseData),
                            SBC_SENSE_KEY_NO_SENSE,
                            0,
                            0);

        /* Is command complete ? */
        if (status == MSDD_STATUS_SUCCESS) {

            isCommandComplete = 1;
        }
    }

    /* Check if command has been completed */
    if (isCommandComplete) {

        TRACE_INFO_WP("Cplt ");

        /* Adjust data residue */
        if (commandState->length != 0) {

            csw->dCSWDataResidue += commandState->length;

            /* STALL the endpoint waiting for data */
            if ((cbw->bmCBWFlags & MSD_CBW_DEVICE_TO_HOST) == 0) {

                /* Stall the OUT endpoint : host to device */
                /* MSDD_Halt(MSDD_CASE_STALL_OUT); */
                commandState->postprocess = MSDD_CASE_STALL_OUT;
                TRACE_INFO_WP("StaOUT ");
            }
            else {

                /* Stall the IN endpoint : device to host */
                /* MSDD_Halt(MSDD_CASE_STALL_IN); */
                commandState->postprocess = MSDD_CASE_STALL_IN;
                TRACE_INFO_WP("StaIN ");
            }
        }

        /* Reset command state */
        commandState->state = 0;
    }

    return isCommandComplete;
}

/**
 * State machine for the MSD %device driver
 * \param  pMsdDriver Pointer to a MSDDriver instance
 */
void MSDD_StateMachine(MSDDriver * pMsdDriver)
{
    MSDCommandState *commandState = &(pMsdDriver->commandState);
    MSCbw           *cbw = &(commandState->cbw);
    MSCsw           *csw = &(commandState->csw);
    MSDTransfer     *transfer = &(commandState->transfer);
    unsigned char   status;

    /* Identify current driver state */
    switch (pMsdDriver->state) {
    /*---------------------- */
    case MSDD_STATE_READ_CBW:
    /*---------------------- */
        /* Start the CBW read operation */
        transfer->semaphore = 0;
    #if 1
        status = USBD_Read(commandState->pipeOUT,
                           cbw,
                           MSD_CBW_SIZE,
                           (TransferCallback) MSDDriver_Callback,
                           (void *) transfer);
    #else
        status = MSDD_Read(cbw,
                           MSD_CBW_SIZE,
                           (TransferCallback) MSDDriver_Callback,
                           (void *) transfer);
    #endif

        /* Check operation result code */
        if (status == USBD_STATUS_SUCCESS) {

            /* If the command was successful, wait for transfer */
            pMsdDriver->state = MSDD_STATE_WAIT_CBW;
        }
        break;

    /*---------------------- */
    case MSDD_STATE_WAIT_CBW:
    /*---------------------- */
        /* Check transfer semaphore */
        if (transfer->semaphore > 0) {

            /* Take semaphore and terminate transfer */
            transfer->semaphore--;

            /* Check if transfer was successful */
            if (transfer->status == USBD_STATUS_SUCCESS) {

                TRACE_INFO_WP("------------------------------\n\r");

                /* Process received command */
                pMsdDriver->state = MSDD_STATE_PROCESS_CBW;
            }
            else if (transfer->status == USBD_STATUS_RESET) {

                TRACE_INFO("MSDD_StateMachine: EP resetted\n\r");
                pMsdDriver->state = MSDD_STATE_READ_CBW;
            }
            else {

                TRACE_WARNING(
                    "MSDD_StateMachine: Failed to read CBW\n\r");
                pMsdDriver->state = MSDD_STATE_READ_CBW;
            }
        }
        break;

    /*------------------------- */
    case MSDD_STATE_PROCESS_CBW:
    /*------------------------- */
        /* Check if this is a new command */
        if (commandState->state == 0) {

            /* Copy the CBW tag */
            csw->dCSWTag = cbw->dCBWTag;

            /* Check that the CBW is 31 bytes long */
            if ((transfer->transferred != MSD_CBW_SIZE) ||
                (transfer->remaining != 0)) {

                TRACE_WARNING(
                    "MSDD_StateMachine: Invalid CBW (len %d)\n\r",
                    (int)transfer->transferred);

                /* Wait for a reset recovery */
                pMsdDriver->waitResetRecovery = 1;

                /* Halt the Bulk-IN and Bulk-OUT pipes */
                //MSDD_Halt(MSDD_CASE_STALL_OUT | MSDD_CASE_STALL_IN);
                USBD_Halt(commandState->pipeIN);
                USBD_Halt(commandState->pipeOUT);

                csw->bCSWStatus = MSD_CSW_COMMAND_FAILED;
                pMsdDriver->state = MSDD_STATE_READ_CBW;

            }
            /* Check the CBW Signature */
            else if (cbw->dCBWSignature != MSD_CBW_SIGNATURE) {

                TRACE_WARNING(
                    "MSD_BOTStateMachine: Invalid CBW (Bad signature)\n\r");

                /* Wait for a reset recovery */
                pMsdDriver->waitResetRecovery = 1;

                /* Halt the Bulk-IN and Bulk-OUT pipes */
                //MSDD_Halt(MSDD_CASE_STALL_OUT | MSDD_CASE_STALL_IN);
                USBD_Halt(commandState->pipeIN);
                USBD_Halt(commandState->pipeOUT);

                csw->bCSWStatus = MSD_CSW_COMMAND_FAILED;
                pMsdDriver->state = MSDD_STATE_READ_CBW;
            }
            else {

                /* Pre-process command */
                MSDD_PreProcessCommand(pMsdDriver);
            }
        }

        /* Process command */
        if (csw->bCSWStatus == MSDD_STATUS_SUCCESS) {

            if (MSDD_ProcessCommand(pMsdDriver)) {

                /* Post-process command if it is finished */
                if (MSDD_PostProcessCommand(pMsdDriver)) {

                    TRACE_INFO_WP("WaitHALT ");
                    pMsdDriver->state = MSDD_STATE_WAIT_HALT;
                }
                else {

                    pMsdDriver->state = MSDD_STATE_SEND_CSW;
                }
            }
            TRACE_INFO_WP("\n\r");
        }

        break;

    /*---------------------- */
    case MSDD_STATE_SEND_CSW:
    /*---------------------- */
        /* Set signature */
        csw->dCSWSignature = MSD_CSW_SIGNATURE;

        /* Start the CSW write operation */
      #if 1
        status = USBD_Write(commandState->pipeIN,
                            csw,
                            MSD_CSW_SIZE,
                            (TransferCallback) MSDDriver_Callback,
                            (void *) transfer);
      #else
        status = MSDD_Write(csw,
                            MSD_CSW_SIZE,
                            (TransferCallback) MSDDriver_Callback,
                            (void *) transfer);
      #endif

        /* Check operation result code */
        if (status == USBD_STATUS_SUCCESS) {

            TRACE_INFO_WP("SendCSW ");

            /* Wait for end of transfer */
            pMsdDriver->state = MSDD_STATE_WAIT_CSW;
        }
        break;

    /*---------------------- */
    case MSDD_STATE_WAIT_CSW:
    /*---------------------- */
        /* Check transfer semaphore */
        if (transfer->semaphore > 0) {

            /* Take semaphore and terminate transfer */
            transfer->semaphore--;

            /* Check if transfer was successful */
            if (transfer->status == USBD_STATUS_RESET) {

                TRACE_INFO("MSDD_StateMachine: EP resetted\n\r");
            }
            else if (transfer->status == USBD_STATUS_ABORTED) {

                TRACE_WARNING(
                    "MSDD_StateMachine: Failed to send CSW\n\r");
            }
            else {

                TRACE_INFO_WP("ok");
            }

            /* Read new CBW */
            pMsdDriver->state = MSDD_STATE_READ_CBW;
        }
        break;

    /*---------------------- */
    case MSDD_STATE_WAIT_HALT:
    /*---------------------- */
        //if (MSDD_IsHalted() == 0) {
        if (!USBD_IsHalted(commandState->pipeIN)) {

            pMsdDriver->state = MSDD_STATE_SEND_CSW;
        }
        break;
    }
}

/**@}*/
